/**
* AutoSWF - Cody Cushing. March 26, 2009
* Copyright (c) 2009 Cody Cushing
* No use restrictions. Maintain this notice in all versions. 
**/

package com.autoswf.tracker
{
	import com.autoswf.core.ChainLink;
	import com.autoswf.core.Transport;
	
	import flash.utils.Dictionary;

	/**
	 * Declarative tracking interpreter. Correlates application events with xml data for 
	 * dynamic property retrieval, url formatting and nested triggering. 
	 */
	public class AsyncTracker extends ChainLink
	{
		protected var pendingTracks:Array;
		protected var pendingTrackIds:Array;
		
		protected var clients:Dictionary;
		public function addClient(client:ITrackClient, id:String):void
		{
			clients[id] = client;
		}
		
		protected var _definitions:XML = <definitions/>
		public function get definitions():XML
		{
			return _definitions;
		}
		
		
		/**
		 * Add an xml tracking definition to the collection. 
		 */		
		public function addDefinition(clientDefinition:XML):void
		{
			if(clientDefinition.name() != "definition")
			{
				throw new TypeError("not a valid tracking definintion");
			}
			_definitions.appendChild(clientDefinition);
		}
		
		/**
		 * Constructor
		 */
		public function AsyncTracker(id:String)
		{
			super(id);
			clients = new Dictionary();
			pendingTracks = [];
			pendingTrackIds = [];
		}
		
		
		/**
		 * Extract id from transport, pass to interpret functions. 
		 */
		override public function sendUpChain(param:*):void
		{
			var transport:Transport = Transport(param);
			var id:String = transport.toString();
			
			executePendingTracks(id);
			executeNewTracks(id);
		}
		
		
		/**
		 * Collect nodes with matching ids from each definition, create track objects, imbue with 
		 * global properties, pass to compile() for processing. 
		 */
		protected function executeNewTracks(id:String):void
		{
			var nodes:XMLList = _definitions.definition.node.(@trigger == id);
			var length:int = nodes.length();
			for(var i:int = 0; i < length; i++)
			{
				var node:XML = nodes[i];
				var track:Track = new Track(node);    // init parent node with global properties
				track.clientId = node.parent().@handler.toString();
				track.str = node.parent().@pattern;
				
				compile(node, track);				
				send(track);
			}
		}
		
		
		/**
		 * Pull matching asyncronous triggers from collection, pass to compile() for processing.
		 */
		protected function executePendingTracks(id:String):void
		{
			var index:int;
			while( -1  !=  (index = pendingTrackIds.indexOf(id)) )
			{
				pendingTrackIds.splice(index, 1);
				var track:Track = pendingTracks.splice(index, 1)[0];
				var superTrack:Track = track.superTrack;
				superTrack.pendingTracks.splice(superTrack.pendingTracks.indexOf(track), 1);
				
				compile(track.node, superTrack);
				send(superTrack);
			}
		}
		
		
		/**
		 * Send the track via the track sending strategy. 
		 */		
		protected function send(track:Track):void
		{
			if(track.pendingTracks.length == 0)
			{
				var trackClient:ITrackClient = clients[track.clientId];
				trackClient.send(track.str);
			}
		}
		
		
		/**
		 * Process node, checking for child nodes, writing properties and constants to pattern, 
		 * registering nested nodes for asyncronous triggering. 
		 */
		protected function compile(parentNode:XML, superTrack:Track):void
		{
			superTrack.str = superTrack.str.replace(parentNode.@id, parentNode.@pattern);
			var node:XML;
			
			/* populate string pattern with child nodes */
			var childNodes:XMLList = parentNode.children();
			var length:int = childNodes.length();
			for(var index:int = 0; index < length; index++)
			{
				node = childNodes[index];
				var name:String = node.name();
					
				if(name == "node")
				{
					var track:Track = new Track(node, superTrack);
					superTrack.pendingTracks.push(track);
					pendingTracks.push(track);
					pendingTrackIds.push(node.@trigger.toString());
					continue;
				}
				
				if(name == "property")
				{
					superTrack.str = retrieveValue(superTrack.str, node.@id, node.@path);
					continue;
				}
				
				if(name == "constant")
				{
					superTrack.str = superTrack.str.replace(node.@id, node.@value.toString());
				}
			}
			
			/* populate remaining tokens in string pattern from parent node */
			var globalProperties:XMLList = parentNode.parent().property;
			var tokens:Array = superTrack.str.match(/\{.*?\}/g);
			for each(var token:String in tokens)
			{
				node = globalProperties.(@id == token)[0];
				if(node)
				{				
					superTrack.str = retrieveValue(superTrack.str, token, node.@path);
				}
			}
		}
		
		
		/**
		 * Get a value from an address in the graph. 
		 */		
		protected function retrieveValue(pattern:String, token:String, path:String):String
		{
			var result:String = sendDownChain(path);                          // get property from graph
			result = (result) ? result.split(DELIMITER).reverse()[0] : null;
			return inject(token, result, pattern);
		}
		
		
		/**
		 * Repalce a token with a literal.
		 */		
		protected function inject(token:String, value:String, pattern:String):String
		{
			token = token.replace("{", "\\{").replace("}", "\\}");            // escape for regex
			return pattern.replace(new RegExp(token, "g"), value);
		}
	}
}


internal class Track
{
	public var node:XML;
	public var superTrack:Track;
	public var str:String;
	public var clientId:String;
	public var pendingTracks:Array = [];
		
	public function Track(node:XML, superTrack:Track = null)
	{
		this.node = node;
		this.superTrack = superTrack;
	}
}

